package c.c.d.s.s.o.da.rs.configuration.support.accesscontrol;

import cn.caplike.data.redis.service.spring.boot.starter.RedisKey;
import cn.caplike.data.redis.service.spring.boot.starter.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Description: 自定义的 {@link FilterInvocationSecurityMetadataSource}<br>
 * Details: SecurityMetadataSource 的提供的 Configuration Attributes 正是 AccessDecisionManager 的判断依据
 * (ref: org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation(Object))
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2020-07-27 17:58
 */
@Slf4j
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    public static final String AT = "@";

    private static final String METADATA_RESOURCE_ADDRESS_CACHE_PREFIX = "metadata.resource-address";

    private static final String CLIENT_ACCESS_SCOPE = "client-access-scope";

    private static final String CLIENT_AUTHORITY = "client-authority";

    private static final String USER_AUTHORITY = "user-authority";

    /**
     * {@link ConfigAttribute} 配置属性的前缀: 客户端访问范围
     */
    public static final String CONFIG_ATTR_PREFIX_CLIENT_ACCESS_SCOPE = CLIENT_ACCESS_SCOPE + AT;

    /**
     * {@link ConfigAttribute} 配置属性的前缀: 客户端职权
     */
    public static final String CONFIG_ATTR_PREFIX_CLIENT_AUTHORITY = CLIENT_AUTHORITY + AT;

    /**
     * {@link ConfigAttribute} 配置属性的前缀: 用户端职权
     */
    public static final String CONFIG_ATTR_PREFIX_USER_AUTHORITY = USER_AUTHORITY + AT;

    // =================================================================================================================

    /**
     * 缓存键: 客户端访问范围-资源路径元数据
     */
    private static final RedisKey METADATA_CLIENT_ACCESS_SCOPE_RESOURCE_ADDRESS_CACHE_KEY =
            RedisKey.builder().prefix(METADATA_RESOURCE_ADDRESS_CACHE_PREFIX).suffix(CLIENT_ACCESS_SCOPE).build();

    /**
     * 缓存键: 客户端职权-资源路径元数据
     */
    private static final RedisKey METADATA_CLIENT_AUTHORITY_RESOURCE_ADDRESS_CACHE_KEY =
            RedisKey.builder().prefix(METADATA_RESOURCE_ADDRESS_CACHE_PREFIX).suffix(CLIENT_AUTHORITY).build();

    /**
     * 缓存键: 用户端职权-资源路径元数据
     */
    private static final RedisKey METADATA_USER_AUTHORITY_RESOURCE_ADDRESS_CACHE_KEY =
            RedisKey.builder().prefix(METADATA_RESOURCE_ADDRESS_CACHE_PREFIX).suffix(USER_AUTHORITY).build();


    // =================================================================================================================

    /**
     * 资源服务 ID
     */
    private String resourceId;

    private RedisService redisService;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        final RedisService.Hash redisServiceOpsForHash = redisService.hash();

        // ~ 由 supports 方法决定
        final FilterInvocation filterInvocation = (FilterInvocation) object;
        final String endpoint = filterInvocation.getRequestUrl();
        // 资源地址
        final String resourceAddress = StringUtils.join(endpoint, AT, Objects.requireNonNull(resourceId, "资源服务器 ID 未定义!"));

        // ~ 通过要访问的端点和当前资源服务器 ID 获取可访问当前资源的 ClientAuthority, UserAuthority 和 ClientAccessScope 集合,
        //   约定, 每一种权限按照约定的前缀放入集合, 便于 AccessDecisionManager.
        //   然后, AccessDecisionManager 根据 OAuth2Authentication 判断 authorities / scopes 是否在集合中

        // ~ [ClientAccessScope]
        final Map<Object, Object> clientAccessScopeResourceAddressMapping = redisServiceOpsForHash.getAll(METADATA_CLIENT_ACCESS_SCOPE_RESOURCE_ADDRESS_CACHE_KEY);
        final Collection<ConfigAttribute> configAttributes = clientAccessScopeResourceAddressMapping.keySet()
                .stream()
                .filter(clientAccessScopeName ->
                        StringUtils.equals(MapUtils.getString(clientAccessScopeResourceAddressMapping, clientAccessScopeName), resourceAddress)
                )
                .map(clientAccessScopeName -> new SecurityConfig(StringUtils.join(CONFIG_ATTR_PREFIX_CLIENT_ACCESS_SCOPE, clientAccessScopeName))).collect(Collectors.toSet());

        // ~ [ClientAuthority]
        final Map<Object, Object> clientAuthorityResourceAddressMapping = redisServiceOpsForHash.getAll(METADATA_CLIENT_AUTHORITY_RESOURCE_ADDRESS_CACHE_KEY);
        configAttributes.addAll(clientAuthorityResourceAddressMapping.keySet()
                .stream()
                .filter(clientAuthorityName ->
                        StringUtils.equals(MapUtils.getString(clientAuthorityResourceAddressMapping, clientAuthorityName), resourceAddress)
                )
                .map(clientAuthorityName -> new SecurityConfig(StringUtils.join(CONFIG_ATTR_PREFIX_CLIENT_AUTHORITY, clientAuthorityName)))
                .collect(Collectors.toSet())
        );

        // ~ [UserAuthority]
        final Map<Object, Object> userAuthorityResourceAddressMapping = redisServiceOpsForHash.getAll(METADATA_USER_AUTHORITY_RESOURCE_ADDRESS_CACHE_KEY);
        configAttributes.addAll(userAuthorityResourceAddressMapping.keySet()
                .stream()
                .filter(userAuthorityName ->
                        StringUtils.equals(MapUtils.getString(userAuthorityResourceAddressMapping, userAuthorityName), resourceAddress)
                )
                .map(userAuthorityName -> new SecurityConfig(StringUtils.join(CONFIG_ATTR_PREFIX_USER_AUTHORITY, userAuthorityName)))
                .collect(Collectors.toSet())
        );

        // ~ 为 AccessDecisionManager 提供包含匹配当前访问的资源端点的 ClientAuthority, UserAuthority, 以及 ClientAccessScope 的集合
        //   格式:
        //       - ClientAccessScope: ClientAccessScope.CACHE_PREFIX@ClientAccessScopeName
        //       - ClientAuthority: ClientAuthority.CACHE_PREFIX@ClientAuthorityName
        //       - UserAuthority: UserAuthority.CACHE_PREFIX@UserAuthorityName
        return configAttributes;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        log.debug("CustomFilterInvocationSecurityMetadataSource :: getAllConfigAttributes");
        throw new UnsupportedOperationException("不支持的操作!");
    }

    @Override
    public boolean supports(Class<?> clazz) {
        log.debug("CustomFilterInvocationSecurityMetadataSource :: supports :: {}", clazz.getCanonicalName());
        // ~ FilterInvocation: 持有与 HTTP 过滤器相关的对象
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

    // =================================================================================================================

    // ~ Autowired
    // -----------------------------------------------------------------------------------------------------------------

    @Autowired
    public void setRedisService(RedisService redisService) {
        this.redisService = redisService;
    }

    // =================================================================================================================

    /**
     * 设置资源服务器 ID
     */
    public void setResourceId(String resourceId) {
        this.resourceId = resourceId;
    }
}
